﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Engine.Sgml.XPath;
using System.Diagnostics;

namespace Engine.Sgml {
    public partial class SElement {
        /// <summary>
        /// Selects an element using a XPath expression.
        /// </summary>
        /// <param name="expression">A string that contains a XPath expression.</param>
        /// <returns>A SElement or null.</returns>
        public SElement SelectElement(string expression) {
            Guard.ArgumentNotNull(expression, "expression");
            SgmlNavigator navigator = new SgmlNavigator(this);
            return navigator.SelectElement(expression);
            }

        /// <summary>
        /// Selects an attribute using a XPath expression.
        /// </summary>
        /// <param name="expression">A string that contains a XPath expression.</param>
        /// <returns>A SAttribute or null.</returns>
        public SAttribute SelectAttribute(string expression) {
            Guard.ArgumentNotNull(expression, "expression");
            SgmlNavigator navigator = new SgmlNavigator(this);
            return navigator.SelectAttribute(expression);
            }

        /// <summary>
        /// Selects a collection of elements using a XPath expression.
        /// </summary>
        /// <param name="expression">A string that contains a XPath expression.</param>
        /// <returns>An IEnumerable&lt;SElement&gt; that contains the selected elements.</returns>
        public IEnumerable<SElement> SelectElements(string expression) {
            Guard.ArgumentNotNull(expression, "expression");
            SgmlNavigator navigator = new SgmlNavigator(this);
            return navigator.SelectElements(expression);
            }

        /// <summary>
        /// Selects a collection of attributes using a XPath expression.
        /// </summary>
        /// <param name="expression">A string that contains a XPath expression.</param>
        /// <returns>An IEnumerable&lt;SAttribute&gt; that contains the selected elements.</returns>
        public IEnumerable<SAttribute> SelectAttributes(string expression) {
            Guard.ArgumentNotNull(expression, "expression");
            SgmlNavigator navigator = new SgmlNavigator(this);
            return navigator.SelectAttributes(expression);
            }

        /// <summary>
        /// Gets the first attribute of this element.
        /// </summary>
        /// <returns>A SAttribute that contains the first attribute of this element.</returns>
        public SAttribute FirstAttribute {
            get { return Attributes.FirstElement; }
            }

        /// <summary>
        /// Gets the last attribute of this element.
        /// </summary>
        /// <returns>A SAttribute that contains the last attribute of this element.</returns>
        public SAttribute LastAttribute {
            get { return Attributes.LastElement; }
            }

        /// <summary>
        /// Gets the concatenated text contents of this element.
        /// </summary>
        /// <returns>The concatenated values of the node and all its child nodes.</returns>
        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        public string InnerText {
            get {
                if (HasElements) {
                    StringBuilder builder = new StringBuilder();
                    ConcatenateText(this, ref builder);
                    return builder.ToString();
                    }
                else
                    return string.Empty;
                }
            }

        private void ConcatenateText(SElement element, ref StringBuilder builder) {
            foreach (SElement node in element.Nodes) {
                if (node.NodeType == SgmlNodeType.Text)
                    builder.Append(node.Value);
                else if (node.HasElements)
                    ConcatenateText(node, ref builder);
                }
            }

        /// <summary>
        /// Get the first child node of this node.
        /// </summary>
        /// <returns>A SElement containing the first child node of the SElement.</returns>
        public SElement FirstNode {
            get { return Nodes.FirstElement; }
            }

        /// <summary>
        /// Get the last child node of this node.
        /// </summary>
        /// <returns>A SElement containing the last child node of the SElement.</returns>
        public SElement LastNode {
            get { return Nodes.LastElement; }
            }

        /// <summary>
        /// Gets a value indicating whether this element has at least one attribute.
        /// </summary>
        /// <returns>true if this element has at least one attribute; otherwise false.</returns>
        public bool HasAttributes {
            get { return Attributes.Count > 0; }
            }

        /// <summary>
        /// Gets a value indicating whether this element has at least one child element.
        /// </summary>
        /// <returns>true if this element has at least one child element; otherwise false.</returns>
        public bool HasElements {
            get { return Nodes.Count > 0; }
            }

        /// <summary>
        /// Gets a value indicating whether this element has a declaration.
        /// </summary>
        /// <returns>true if this element has a DTD element declaration; otherwise false.</returns>
        public bool HasDeclaration {
            get { return Declaration != null; }
            }

        /// <summary>
        /// Gets a value indicating whether this element defined in a current DTD schema.
        /// </summary>
        /// <returns>true if this element present in a DTD; otherwise false.</returns>
        public bool DefinedInScheme {
            get {
                if (Declaration == null && (DName == null || DName == InclusionNode.Instance.Name))
                    return false;
                else
                    return true;
                }
            }

        /// <summary>
        /// Gets the string that represents the XPath for the specified element.
        /// </summary>
        /// <returns>A string that contain XPath expression.</returns>
        internal string XPath {
            get {
                // TODO: Выполнена простейшая реализация.
                // Метод нуждается в доработке.
                return XPathBuilder.BuildAbsolute(this);
                }
            }

        /// <summary>
        /// Removes this element from its parent.
        /// </summary>
        public void Remove() {
            // If the element is already removed
            if (Parent == null)
                return;
            OnChanging(this, SObjectChangeEventArgs.Remove);
            Parent.Nodes.RemoveAt(OrderIndex);
            OnChanged(this, SObjectChangeEventArgs.Remove);
            Parent = null;
            }

        /// <summary>
        /// Removes the attributes of this element.
        /// </summary>
        public void RemoveAttributes() {
            for (int i = 0; i < Attributes.Count; i++) {
                Attributes[i--].Remove();
                }
            }

        /// <summary>
        /// Removes the child nodes from this element. 
        /// </summary>
        public void RemoveElements() {
            for (int i = 0; i < Nodes.Count; i++) {
                Nodes[i--].Remove();
                }
            }

        /// <summary>
        /// Adds the specified attribute as children of this element.
        /// </summary>
        /// <param name="attribute">An attribute to be added.</param>
        public void Add(SAttribute attribute) {
            Guard.ArgumentNotNull(attribute, "attribute");
            if (attribute.Parent != null)
                attribute.Remove();
            OnChanging(attribute, SObjectChangeEventArgs.Add);
            Attributes.Add(attribute);
            attribute.Parent = this;
            OnChanged(attribute, SObjectChangeEventArgs.Add);
            }

        /// <summary>
        /// Adds the specified element as children of this element.
        /// </summary>
        /// <param name="element">An element to be added.</param>
        public void Add(SElement element) {
            Guard.ArgumentNotNull(element, "element");
            this.Insert(element, Nodes.Count);
            }

        /// <summary>
        /// Adds the specified element as the first children of this element.
        /// </summary>
        /// <param name="element">An element to be added.</param>
        public void AddFirst(SElement element) {
            Guard.ArgumentNotNull(element, "element");
            this.Insert(element, 0);
            }

        /// <summary>
        /// Adds the specified attribute as the first children of this element.
        /// </summary>
        /// <param name="attribute">An attribute to be added.</param>
        public void AddFirst(SAttribute attribute) {
            Guard.ArgumentNotNull(attribute, "attribute");
            if (attribute.Parent != null)
                attribute.Remove();
            OnChanging(attribute, SObjectChangeEventArgs.Add);
            Attributes.Insert(attribute, 0);
            attribute.Parent = this;
            OnChanged(attribute, SObjectChangeEventArgs.Add);
            }

        /// <summary>
        /// Adds the specified element immediately before this node.
        /// </summary>
        /// <param name="element">An element to be added before this node.</param>
        /// <exception cref="InvalidOperationException">This <paramref name="element"/> is not in a document tree.</exception>
        public void AddBeforeSelf(SElement element) {
            Guard.ArgumentNotNull(element, "element");
            Debug.Assert(Parent != null);
            if (Parent == null)
                throw new InvalidOperationException("The node before which adds the specified element is not in a document tree.");
            Parent.Insert(element, OrderIndex);
            }

        /// <summary>
        /// Adds the specified element immediately after this node.
        /// </summary>
        /// <param name="element">An element to be added after this node.</param>
        /// <exception cref="InvalidOperationException">This <paramref name="element"/> is not in a document tree.</exception>
        public void AddAfterSelf(SElement element) {
            Guard.ArgumentNotNull(element, "element");
            Debug.Assert(Parent != null);
            if (Parent == null)
                throw new InvalidOperationException("The node after which adds the specified element is not in a document tree.");
            Parent.Insert(element, OrderIndex + 1);
            }


        /// <summary>
        /// Moves this element to another position in the collection of child nodes of its ancestor.
        /// </summary>
        /// <param name="position">A new index in the collection.</param>
        /// <exception cref="ArgumentOutOfRangeException">Invalid position parameter.</exception>
        /// <exception cref="InvalidOperationException">This element is not in a document tree.</exception>
        public void Move(int position) {
            if (Parent == null)
                throw new InvalidOperationException("This node is not contained by the collection.");
            if (OrderIndex == position)
                return;
            OnChanging(this, SObjectChangeEventArgs.Move);
            Parent.Nodes.Move(OrderIndex, position);
            OnChanged(this, SObjectChangeEventArgs.Move);
            }

        /// <summary>
        /// Gets the next sibling node of this node.
        /// </summary>
        /// <returns>The SElement that contains the next sibling node.</returns>
        public SElement NextNode {
            get {
                if (object.ReferenceEquals(Parent, null) || Parent.Nodes.Count == 0)
                    return null;
                else
                    return Parent.Nodes[OrderIndex + 1];
                }
            }

        /// <summary>
        /// Gets the previous sibling node of this node.
        /// </summary>
        /// <returns>The SElement that contains the previous sibling node.</returns>
        public SElement PreviousNode {
            get {
                if (object.ReferenceEquals(Parent, null) || Parent.Nodes.Count == 0)
                    return null;
                else
                    return Parent.Nodes[OrderIndex - 1];
                }
            }

        /// <summary>
        /// Gets the depth for the current element.
        /// </summary>
        /// <returns>The depth of the current node in the SGML document.</returns>
        public int Depth {
            get {
                int depth = 0;
                SElement parent = Parent;
                while (!object.ReferenceEquals(parent, null)) {
                    depth++;
                    parent = parent.Parent;
                    }
                return depth;
                }
            }

        /// <summary>
        /// Gets the root element of the tree for the document.
        /// </summary>
        /// <returns>The SElement that is root element in the SGML document.</returns>
        public SElement Root {
            get {
                if (Parent == null)
                    return this;
                SElement parent = Parent;
                while (parent != null && parent.NodeType != SgmlNodeType.Document) {
                    parent = parent.Parent;
                    }
                return parent;
                }
            }

        private void Insert(SElement element, int index) {
            Debug.Assert(element != null);
            if (element.Parent != null) {
                OnChanging(element, SObjectChangeEventArgs.Remove);
                element.Parent.Nodes.RemoveAt(element.OrderIndex);
                OnChanged(element, SObjectChangeEventArgs.Remove);
                }
            OnChanging(element, SObjectChangeEventArgs.Add);
            Nodes.Insert(element, index);
            element.Parent = this;
            OnChanged(element, SObjectChangeEventArgs.Add);
            }
        }
    }
